/*
 * OpenParts
 * A dynamic-loading components framework for GWT
 * Copyright (C) 2011 Christophe Bouthier  [chris{AT}binary-gastronome{DOT}fr]
 *
 * This work is partially based on work I have done at INRIA (http://www.inria.fr) 
 * in the context of the Qualipso European Project (http://qualipso.org/),
 * The work done at INRIA is Copyright (C) 2006-2011 INRIA
 *
 * This work is distributed under the LGPL version 3
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation. See the GNU
 * Lesser General Public License in LGPL.txt for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package fr.openparts.OpenParts.rebind;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;


import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.dev.util.collect.HashMap;
import com.google.gwt.user.client.ui.Widget;

import fr.openparts.OpenParts.client.OPParams;
import fr.openparts.OpenParts.client.OPSlotProxy;
import fr.openparts.OpenParts.client.annotations.OPEventHandler;
import fr.openparts.OpenParts.client.annotations.OPMandatoryParams;
import fr.openparts.OpenParts.client.annotations.OPServiceName;
import fr.openparts.OpenParts.client.annotations.OPViewFactory;

/**
 * Manage all necessary data for binder building, taken from parsing parts annotations and annotated data.
 * 
 * @author Christophe Bouthier [chris{AT}binary-gastronome{DOT}fr]
 * @creation.date 26 December 2009
 */
public class OPBinderBuildingData {

    /**
     * The name of the service represented by the part binded.
     */
    protected final String serviceName;

    /**
     * List of event handler callbacks.
     */
    protected final List<OPBinderBuildingDataEventHandler> callbacks;

    /**
     * List of view factories.
     */
    protected final List<OPBinderBuildingDataViewFactory> factories;

    /**
     * Array of all events registered.
     */
    protected final String[] eventsRegistered;

    /**
     * Array of all resources registered.
     */
    protected final String[] resourcesRegistered;

    /**
     * Constructor. Parse the part to find all annotated data and fill up with them the necessary informations for binder's building.
     * 
     * @param part
     *            the part to parse to find annotated data. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @throws UnableToCompleteException
     *             if there is something bad in the part that break up the building information
     */
    public OPBinderBuildingData(final JClassType part, final TreeLogger logger) throws UnableToCompleteException {
        assert part != null : "parameter part shouldn't be null";
        assert logger != null : "parameter logger shoudln't be null";

        serviceName = getServiceNameFromPart(part, logger);
        assert serviceName != null : "service name cannot be null";

        callbacks = new ArrayList<OPBinderBuildingDataEventHandler>();
        factories = new ArrayList<OPBinderBuildingDataViewFactory>();

        parsePartForData(part, logger);

        eventsRegistered = sortAllCallbacks(logger);
        assert eventsRegistered != null : "list of registered events cannot be null";

        resourcesRegistered = sortAllFactories(logger);
        assert resourcesRegistered != null : "list of registered resources cannot be null";
    }

    /**
     * Return the service name for the binder.
     * 
     * @return the service name. Cannot be null.
     */
    public String getServiceName() {
        assert serviceName != null : "serviceName cannot be null";

        return serviceName;
    }

    /**
     * Return the list of event handler callbacks.
     * 
     * @return the list of event handler callbacks. Cannot be null.
     */
    public List<OPBinderBuildingDataEventHandler> getCallbacks() {
        assert callbacks != null : "callbacks cannot be null";

        return callbacks;
    }

    /**
     * Return the list of view factories.
     * 
     * @return the list of view factories. Cannot be null.
     */
    public List<OPBinderBuildingDataViewFactory> getFactories() {
        assert factories != null : "factories cannot be null";

        return factories;
    }

    /**
     * Return an array of all registered events.
     * 
     * @return an array of all registered events. Can be empty, but cannot be null.
     */
    public String[] getAllRegisteredEvents() {
        assert eventsRegistered != null : "eventsRegistered cannot be null";

        return eventsRegistered;
    }

    /**
     * Return an array of all registered resources.
     * 
     * @return an array of all registered resources. Can be empty, but cannot be null.
     */
    public String[] getAllRegisteredResources() {
        assert resourcesRegistered != null : "resourcesRegistered cannot be null";

        return resourcesRegistered;
    }

    /**
     * Get the service name represented by the part, and return it. The service name is found from the value of the {@link OPServiceName} annotation, that
     * annotate the part class. If there is no annotation, then a UnableToCompleteException is thrown.
     * 
     * @param part
     *            the part to parse to find annotation. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @return the service name. Cannot be null.
     * @throws UnableToCompleteException
     *             if the service name cannot be found.
     */
    protected String getServiceNameFromPart(final JClassType part, final TreeLogger logger) throws UnableToCompleteException {
        assert part != null : "parameter part shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        if (!part.isAnnotationPresent(OPServiceName.class)) {
            logger.log(TreeLogger.ERROR, "The part is missing its OPServiceName annotation.");
            throw new UnableToCompleteException();
        }
        assert part.getAnnotation(OPServiceName.class) != null : "OPServiceName annotation should be here";

        final String serviceName = part.getAnnotation(OPServiceName.class).value();
        assert serviceName != null : "service name value of annotation OPServiceName cannot be null";

        return serviceName;
    }

    /**
     * Parse the given part for data about callbacks (methods annotated with {@link OPEventHandler}) and view factories (methods annotated with
     * {@link OPViewFactory}). Throw an UnableToCompleteException if something is wrong.
     * 
     * @param part
     *            the part to parse. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @throws UnableToCompleteException
     *             if there's something wrong.
     */
    protected void parsePartForData(final JClassType part, final TreeLogger logger) throws UnableToCompleteException {
        assert part != null : "parameter part shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        final JMethod[] methods = part.getMethods();
        for (JMethod method : methods) {
            checkMethodForCallbackData(method, logger);
            checkMethodForViewFactoryData(method, logger);
        }
    }

    /**
     * Check the given method for callback data, that is if the method is annotated with the {@link OPEventHandler} annotation, and if it has the correct
     * signature (the annotated method should take one and only one parameter, of type String). Throw an UnableToCompleteException if something is wrong.
     * 
     * @param method
     *            the method to check. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @throws UnableToCompleteException
     *             if something is wrong (bad signature for an annotated method).
     */
    protected void checkMethodForCallbackData(final JMethod method, final TreeLogger logger) throws UnableToCompleteException {
        assert method != null : "parameter method shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        if (!method.isAnnotationPresent(OPEventHandler.class)) {
            return;
        }
        assert method.getAnnotation(OPEventHandler.class) != null : "OPEventHandler method should be here";

        final String[] eventsNames = method.getAnnotation(OPEventHandler.class).value();
        assert eventsNames != null : "events names value of annotation OPEventHandler cannot be null";

        checkThatCallbackHasCorrectSignature(method, logger);

        // ok, the method is a correct callback. Register it, we will sort them all after
        callbacks.add(new OPBinderBuildingDataEventHandler(method, eventsNames));
    }

    /**
     * Check the given method for view factory data, that is if the method is annotated with the {@link OPViewFactory} annotation, and if it has the correct
     * signature (the annotated method should take two parameters, one of type {@link OPParams}, one of type {@link OPSlotProxy}). Throw an
     * UnableToCompleteException if something is wrong.
     * 
     * @param method
     *            the method to check. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @throws UnableToCompleteException
     *             if something is wrong (bad signature for an annotated method, no value for slotId, bad annotations without OPViewFactory).
     */
    protected void checkMethodForViewFactoryData(final JMethod method, final TreeLogger logger) throws UnableToCompleteException {
        assert method != null : "parameter method shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        if (!method.isAnnotationPresent(OPViewFactory.class)) {
            if (method.isAnnotationPresent(OPMandatoryParams.class)) {
                logger.log(TreeLogger.ERROR,
                        "The OPMandatoryParams annotation cannot be used without the OPViewFactory one, on the method  " + method.getJsniSignature());
                throw new UnableToCompleteException();
            }
            return;
        }
        assert method.getAnnotation(OPViewFactory.class) != null : "OPViewFactory method should be here";

        final String resourceName = method.getAnnotation(OPViewFactory.class).resourceName();
        final String actionName = method.getAnnotation(OPViewFactory.class).actionName();
        assert resourceName != null : "resource name value of annotation OPViewFactory cannot be null";
        assert actionName != null : "action name value of annotation OPViewFactory cannot be null";

        final String[] mandatoryParams;
        if (method.isAnnotationPresent(OPMandatoryParams.class)) {
            mandatoryParams = method.getAnnotation(OPMandatoryParams.class).value();
        } else {
            mandatoryParams = new String[0];
        }
        assert mandatoryParams != null : "mandatory params value of annotation OPMandatoryParams cannot be null";

        checkThatViewFactoryHasCorrectSignature(method, logger);

        // ok, the method is a correct view factory. Register it, we will sort them all after
        factories.add(new OPBinderBuildingDataViewFactory(method, getServiceName(), resourceName, actionName, mandatoryParams));
    }

    /**
     * Check that the given callback have the correct signature, that is that it takes two parameters, one of type String (the event), the second of type
     * OPParams (the parameters).
     * 
     * @param callback
     *            the callback to check. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @throws UnableToCompleteException
     *             if the signature of the callback is not correct.
     */
    protected void checkThatCallbackHasCorrectSignature(final JMethod callback, final TreeLogger logger) throws UnableToCompleteException {
        assert callback != null : "parameter callback shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        final JParameter[] params = callback.getParameters();
        if (params.length != 2) {
            logger.log(TreeLogger.ERROR, "The callback " + callback.getJsniSignature() + " don't have the correct signature. It should take two parameters.");
            throw new UnableToCompleteException();
        }

        final JParameter paramEvent = params[0];
        assert paramEvent != null : "first parameter cannot be null";

        if (!"java.lang.String".equals(paramEvent.getType().getQualifiedSourceName())) {
            logger.log(TreeLogger.ERROR, "The callback " + callback.getJsniSignature()
                    + " don't have the correct signature. Its first parameter should be of type String.");
            throw new UnableToCompleteException();
        }

        final JParameter paramParams = params[1];
        assert paramParams != null : "second parameter cannot be null";

        if (!"fr.openparts.OpenParts.client.OPParams".equals(paramParams.getType().getQualifiedSourceName())) {
            logger.log(TreeLogger.ERROR, "The callback " + callback.getJsniSignature()
                    + " don't have the correct signature. Its second parameter should be of type OPParams.");
            throw new UnableToCompleteException();
        }
    }

    /**
     * Check that the given view factory have the correct signature, that is that it takes two parameters, one of type {@link OPParams}, one of type
     * {@link OPSlotProxy}, and that its return value is of type {@link Widget}.
     * 
     * @param factory
     *            the view factory to check. Shouldn't be null.
     * @param logger
     *            the logger. Shouldn't be null.
     * @throws UnableToCompleteException
     *             if the signature of the factory is not correct.
     */
    protected void checkThatViewFactoryHasCorrectSignature(final JMethod factory, final TreeLogger logger) throws UnableToCompleteException {
        assert factory != null : "parameter factory shouldn't be null";
        assert logger != null : "parameter logger shouldn't be null";

        final JParameter[] params = factory.getParameters();
        if (params.length != 2) {
            logger.log(TreeLogger.ERROR, "The view factory " + factory.getJsniSignature()
                    + " don't have the correct signature. It should take two parameters.");
            throw new UnableToCompleteException();
        }

        final JParameter paramParams = params[0];
        assert paramParams != null : "first parameter cannot be null";

        if (!"fr.openparts.OpenParts.client.OPParams".equals(paramParams.getType().getQualifiedSourceName())) {
            logger.log(TreeLogger.ERROR, "The view factory " + factory.getJsniSignature()
                    + " don't have the correct signature. Its first parameter should be of type OPParams.");
            throw new UnableToCompleteException();
        }

        final JParameter paramProxy = params[1];
        assert paramProxy != null : "second parameter cannot be null";

        if (!"fr.openparts.OpenParts.client.OPSlotProxy".equals(paramProxy.getType().getQualifiedSourceName())) {
            logger.log(TreeLogger.ERROR, "The view factory " + factory.getJsniSignature()
                    + " don't have the correct signature. Its second parameter should be of type OPSlotProxy.");
            throw new UnableToCompleteException();
        }
        
        final JType returnType = factory.getReturnType();
        assert returnType != null : "return type cannot be null";

        if (!"com.google.gwt.user.client.ui.Widget".equals(returnType.getQualifiedSourceName())) {
            logger.log(TreeLogger.ERROR, "The view factory " + factory.getJsniSignature()
                    + " don't have the correct signature. Its return value should be of type Widget.");
            throw new UnableToCompleteException();
        }
    }

    /**
     * Sorts all registered events to check that there is no duplicates, either in all events callbacks or in specific events. Returns an array of all
     * registered events.
     * 
     * @param logger
     *            the logger. Shouldn't be null.
     * @return an array containing all registered events. Can be empty, but cannot be null.
     * @throws UnableToCompleteException
     *             if there's duplicates.
     */
    protected String[] sortAllCallbacks(final TreeLogger logger) throws UnableToCompleteException {
        assert logger != null : "parameter logger shouldn't be null";

        final HashMap<String, String> sortedEvents = new HashMap<String, String>();
        boolean callbackForAllEventsFound = false;

        for (OPBinderBuildingDataEventHandler handler : callbacks) {
            final String[] events = handler.getEvents();

            if (events.length == 0) {
                // callback for all events
                if (callbackForAllEventsFound) {
                    // duplicates all events
                    logger.log(TreeLogger.ERROR, "There should not be several event handler callbacks registering for all events.");
                    throw new UnableToCompleteException();
                }

                callbackForAllEventsFound = true;
                continue;
            } else {
                // callback for specific event
                for (String event : events) {
                    if (sortedEvents.containsKey(event)) {
                        // duplicates specific events
                        logger.log(TreeLogger.ERROR, "There should not be several event handler callbacks registering for the same event (" + event + ").");
                        throw new UnableToCompleteException();
                    }

                    sortedEvents.put(event, event);
                }
            } // end if
        }

        final Set<String> rEvents = sortedEvents.keySet();
        return rEvents.toArray(new String[rEvents.size()]);
    }

    /**
     * Sorts all resources and actions views to check that there is no duplicates. Returns an array of all registered resources.
     * 
     * @param logger
     *            the logger. Shouldn't be null.
     * @return an array containing all registered resources. Can be empty, but cannot be null.
     * @throws UnableToCompleteException
     *             if there's duplicates.
     */
    protected String[] sortAllFactories(final TreeLogger logger) throws UnableToCompleteException {
        assert logger != null : "parameter logger shouldn't be null";

        final HashMap<String, HashMap<String, String>> sortedResources = new HashMap<String, HashMap<String, String>>();
        for (OPBinderBuildingDataViewFactory factory : factories) {
            final String resourceName = factory.getResourceName();
            final String actionName = factory.getActionName();

            if (sortedResources.containsKey(resourceName)) {
                final HashMap<String, String> sortedActions = sortedResources.get(resourceName);
                if (sortedActions.containsKey(actionName)) {
                    // duplicate view factory for the same resource/action tuple
                    logger.log(TreeLogger.ERROR, "There should not be several view factories registering for the same resource name (" + resourceName
                            + ") and action name (" + actionName + ").");
                    throw new UnableToCompleteException();
                }

                sortedActions.put(actionName, actionName);
            } else {
                final HashMap<String, String> sortedActions = new HashMap<String, String>();
                sortedActions.put(actionName, actionName);
                sortedResources.put(resourceName, sortedActions);
            } // end if
        }

        final Set<String> rResources = sortedResources.keySet();
        return rResources.toArray(new String[rResources.size()]);
    }

}
